package com.wdy.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class CacheUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time, unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
    }

    //缓存穿透版本
    public <R> R queryByIdWithCachePenetration(Class<R> resultType, Long id, String redisKeyPrefix, Long expireTime,
                                               TimeUnit unit, Function<Long, R> dbFallback) {
        String redisKey = redisKeyPrefix + id;
        //1.从redis查询缓存
        String objectJson = stringRedisTemplate.opsForValue().get(redisKey);

        //2.是否存在
        if (StrUtil.isNotBlank(objectJson)) {
            //3.存在直接返回
            return JSONUtil.toBean(objectJson, resultType);
        }
        //是否存在空数据
        if ("".equals(objectJson)) {
            return null;
        }

        //4.不存在，从数据库查询
        R result = dbFallback.apply(id);
        //5.数据库里没有,写入空数据,防止缓存穿透
        if (result==null){
            stringRedisTemplate.opsForValue().set(redisKey, "",1, TimeUnit.MINUTES);
        }
        //6.存在，写入缓存
        stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr( result),expireTime, unit);
        return result;
    }

    public <R> R queryByIdWithCacheBreakDown(Class<R> resultType, Long id, String redisKeyPrefix,String lockKeyPrefix,
                                             Long expireTime, TimeUnit unit, Function<Long, R> dbFallback) {
        String redisKey = redisKeyPrefix + id;
        //1.从redis查询缓存
        String objectJson = stringRedisTemplate.opsForValue().get(redisKey + id);

        //2.是否存在非空数据
        if (StrUtil.isNotBlank(objectJson)) {
            //3.存在直接返回
            return JSONUtil.toBean(objectJson, resultType);
        }
        //是否存在空数据
        if ("".equals(objectJson)) {
            return null;
        }


        String lockKey= lockKeyPrefix + id;
        R result;
        //缓存没命中,尝试获取锁
        try {
            boolean isLock = tryLock(lockKey);
            //判断是否获取到锁
            if (!isLock) {
                //获取锁失败，休眠并重试
                Thread.sleep(50);
                return queryByIdWithCacheBreakDown(resultType,id,lockKey,lockKeyPrefix,30L,TimeUnit.MINUTES,dbFallback);
            }
            //获取成功,再查一次刚才是不是有别的线程已经重建过缓存
            objectJson = stringRedisTemplate.opsForValue().get(redisKey + id);
            if (StrUtil.isNotBlank(objectJson)) {
                //3.存在直接返回
                return JSONUtil.toBean(objectJson, resultType);
            }
            if ("".equals(objectJson)) {
                return null;
            }

            //经过确认确实需要重建缓存
            //根据id查询数据库
            result=dbFallback.apply(id);
            //5.数据库里没有,写入空数据,防止缓存穿透
            if (result==null){
                stringRedisTemplate.opsForValue().set(redisKey, "",1, TimeUnit.MINUTES);
            }
            //6.存在，写入缓存
            stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(result),expireTime,unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放锁
            unLock(lockKey);
        }
        return result;
    }

    private final static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public <R> R queryByIdWithLogicalExpire(Class<R> resultType, Long id, String redisKeyPrefix,String lockKeyPrefix,
                                            Long expireTime, TimeUnit unit, Function<Long, R> dbFallback) {
        RedisData redisData;
        String redisKey = redisKeyPrefix + id;
        //1.从redis查询缓存
        String redisDataJson = stringRedisTemplate.opsForValue().get(redisKey + id);

        //缓存里没有,说明不是热点数据,可以直接从数据库查询
        if (StrUtil.isBlank(redisDataJson)) {
            //根据id查询数据库
            return dbFallback.apply(id);
        }
        //缓存里有
        redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        R result = JSONUtil.toBean((JSONObject) redisData.getData(), resultType);
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            //未过期,返回数据
            return result;
        }
        //已过期,尝试获取锁
        String lockKey= lockKeyPrefix + id;
        boolean isLock = tryLock(lockKey);
        //获取到锁
        if (isLock) {
            //确认一下是否已经有别的线程重建过了
            if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                //未过期,说明已经有别的线程重建过了
                return result;
            }

            //去数据库查询店铺数据
            redisData.setData(dbFallback.apply( id));
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(30*60L));
            final RedisData finalRedisData = redisData;
            //开启一个新线程去重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    stringRedisTemplate.opsForValue().set(redisKey,JSONUtil.toJsonStr(finalRedisData));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    unLock(lockKey);
                }
            });
        }
        //重建成功返回数据,或者是没获取到锁直接返回旧数据
        return result;
    }
}
